﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.Threading;
using System.Drawing;
using System.Drawing.Imaging;
using DirectShowLib;
using ARCam3D.Imaging;

namespace ARCam3D.Camera
{
    public class Grabber : ISampleGrabberCB, IDisposable
    {
        private const int bytesPerPixel = 3;

        private IFilterGraph2 graph;
        private IMediaControl mediaControl;
        private IMediaEventEx mediaEvent;

        private IBaseFilter sourceFilter;
        private ISampleGrabber sampleGrabber;

        private int hr;

        private bool running = false;

        private int sampleWidth;
        private int sampleHeight;

        private IntPtr samplePtr;
        private AutoResetEvent newSampleReceivedEvent = new AutoResetEvent(false);


        public static string[] GetDevices()
        {
            DsDevice[] dsDevices = DsDevice.GetDevicesOfCat(FilterCategory.VideoInputDevice);
            string[] devices = new string[dsDevices.Length];

            for (int i = 0; i < dsDevices.Length; ++i)
            {
                devices[i] = dsDevices[i].Name;
            }

            return devices;
        }

        public Grabber(int deviceIndex)
        {
            ICaptureGraphBuilder2 captureGraphBuilder = null;
            try
            {
                //setup sample grabber
                AMMediaType requiredMediaType = new AMMediaType();
                requiredMediaType.majorType = MediaType.Video;
                requiredMediaType.subType = MediaSubType.RGB24;

                sampleGrabber = new SampleGrabber() as ISampleGrabber;
                hr = sampleGrabber.SetMediaType(requiredMediaType);
                DsError.ThrowExceptionForHR(hr);
                hr = sampleGrabber.SetBufferSamples(true);
                DsError.ThrowExceptionForHR(hr);
                hr = sampleGrabber.SetCallback(this, 1);
                DsError.ThrowExceptionForHR(hr);

                //setup graph
                graph = new FilterGraph() as IFilterGraph2;
                mediaControl = graph as IMediaControl;
                mediaEvent = graph as IMediaEventEx;

                hr = graph.AddSourceFilterForMoniker(
                    DsDevice.GetDevicesOfCat(FilterCategory.VideoInputDevice)[deviceIndex].Mon,
                    null,
                    "Source Filter",
                    out sourceFilter);
                DsError.ThrowExceptionForHR(hr);

                hr = graph.AddFilter(
                    sampleGrabber as IBaseFilter,
                    "Sample Grabber");
                DsError.ThrowExceptionForHR(hr);


                //connect graph
                captureGraphBuilder = new CaptureGraphBuilder2() as ICaptureGraphBuilder2;

                hr = captureGraphBuilder.SetFiltergraph(graph);
                DsError.ThrowExceptionForHR(hr);
                hr = captureGraphBuilder.RenderStream(
                        PinCategory.Capture,
                        MediaType.Video,
                        sourceFilter,
                        null,
                        sampleGrabber as IBaseFilter);
                DsError.ThrowExceptionForHR(hr);

                //get image info
                AMMediaType mediaType = new AMMediaType();
                hr = sampleGrabber.GetConnectedMediaType(mediaType);
                DsError.ThrowExceptionForHR(hr);

                VideoInfoHeader videoInfoHeader = Marshal.PtrToStructure(mediaType.formatPtr, typeof(VideoInfoHeader)) as VideoInfoHeader;
                sampleWidth = videoInfoHeader.BmiHeader.Width;
                sampleHeight = videoInfoHeader.BmiHeader.Height;
            }
            finally
            {
                if (captureGraphBuilder != null)
                {
                    Marshal.ReleaseComObject(captureGraphBuilder);
                    captureGraphBuilder = null;
                }
            }
        }

        ~Grabber()
        {
            Dispose();
        }

        public void Dispose()
        {
            if (graph != null)
            {
                Marshal.ReleaseComObject(graph);
                graph = null;
            }

            if (sourceFilter != null)
            {
                Marshal.ReleaseComObject(sourceFilter);
                sourceFilter = null;
            }

            if (sampleGrabber != null)
            {
                Marshal.ReleaseComObject(sampleGrabber);
                sampleGrabber = null;
            }
        }

        public void Start()
        {
            if (!running)
            {
                hr = mediaControl.Run();
                DsError.ThrowExceptionForHR(hr);

                FilterState filterState;
                hr = mediaControl.GetState(0, out filterState);
                DsError.ThrowExceptionForHR(hr);

                if (filterState == FilterState.Running)
                {
                    running = true;
                }
            }
        }

        public void Stop()
        {
            if (running)
            {
                hr = mediaControl.Stop();
                DsError.ThrowExceptionForHR(hr);

                FilterState filterState;
                hr = mediaControl.GetState(0, out filterState);
                DsError.ThrowExceptionForHR(hr);

                if (filterState != FilterState.Running)
                {
                    running = false;
                }
            }
        }

        public RGBImage GetImage()
        {
            IntPtr sampleCopyPtr = Marshal.AllocHGlobal(sampleWidth * sampleHeight * bytesPerPixel * sizeof(byte));
            lock (this)
            {
                Utils.Copy(sampleCopyPtr, samplePtr, sampleWidth * sampleHeight * bytesPerPixel * sizeof(byte));
            }
            flipY(sampleCopyPtr);
            return new RGBImage(sampleWidth, sampleHeight, sampleCopyPtr);
        }

        public void WaitForNewImage()
        {
            newSampleReceivedEvent.WaitOne();
        }

        unsafe private void flipY(IntPtr ptr)
        {
            byte temp;
            byte* bytePtr = (byte*)ptr.ToPointer();

            for (int y = 0; y < sampleHeight / 2; ++y)
            {
                for (int i = 0; i < sampleWidth * bytesPerPixel; ++i)
                {
                    temp = bytePtr[y * sampleWidth * bytesPerPixel + i];
                    bytePtr[y * sampleWidth * bytesPerPixel + i] = bytePtr[(sampleHeight - y - 1) * sampleWidth * bytesPerPixel + i];
                    bytePtr[(sampleHeight - y - 1) * sampleWidth * bytesPerPixel + i] = temp;
                }
            }
        }

        int ISampleGrabberCB.SampleCB(double sampleTime, IMediaSample sample)
        {
            return 0;
        }

        int ISampleGrabberCB.BufferCB(double sampleTime, IntPtr bufferPtr, int bufferLength)
        {
            lock (this)
            {
                samplePtr = bufferPtr;
            }
            newSampleReceivedEvent.Set();
            return 0;
        }
    }
}
